package org.chorus;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;

import jwo.landserf.gui.GISFrame;
import jwo.landserf.gui.SimpleGISFrame;
import jwo.landserf.process.ContourThread;
import jwo.landserf.process.SurfParam;
import jwo.landserf.structure.ColourTable;
import jwo.landserf.structure.Footprint;
import jwo.landserf.structure.RasterMap;
import jwo.landserf.structure.SpatialModel;
import jwo.landserf.structure.VectorMap;

import org.chorus.DisplayUtils.LayoutType;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PGraphics;
import processing.core.PImage;

//  **********************************************************************************
/** Module for showing surface inclinations over a range of scales and directions.
 *  @author Jo Wood
 *  @version 1.3.2, 18th March, 2013.
 */
//  **********************************************************************************

/* This file is part of the Chorus river bed analysis software package. Chorus is free software:
 * you can redistribute it and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * Chorus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this
 * source code (see COPYING included with this source code). If not, see 
 * http://www.gnu.org/licenses/.
 */

public class InclinationChart2D implements Modular, Comparable<Modular>
{
	// ----------------------------- Object variables ------------------------------

	private HashMap<Integer,PImage> inclinationImgs;// Graphical representations of the inclination chart.
	private HashMap<Integer,VectorMap>contourMaps;	// Vector representation of inclination chart contours.
	private HashMap<Integer,String> titles;			// Titles of the rasters used to generate the inclination charts.
	private int nextIndex;							// Index of the next DEM to add (ensures order is independent of thread completion speed).
	private ArrayList<InclinationBuilder>builders;	// List of all active inclination calculation processes.
	private PApplet parent;							// Parent sketch in which this module is embedded.
	private PGraphics graphics;						// Graphics context in which to direct output.
	private int maxLagCells;						// Maximum lag in raster cells.
	private Waiter waiter;							// Used to provide a dynamic 'waiting' symbol while inclination is calculated.
	private int colourOffset;				    	// Used to select starting palette colour
	private int order;								// Order number for sorted modules.
	
												
	// ---------------------------------- Methods ----------------------------------

	/** Initialises the inclination chart sketch.
	 */
	public void setup(PApplet parent)
	{
		this.parent = parent;
		this.graphics = parent.g;
		inclinationImgs = new HashMap<Integer, PImage>();
		contourMaps  = new HashMap<Integer, VectorMap>();
		titles = new HashMap<Integer, String>();
		builders = new ArrayList<InclinationChart2D.InclinationBuilder>();
		colourOffset = 0;
		nextIndex = 0;
	}
	
	/** Sets the the graphics context into which all output is directed. This method allows
	 *  output to be redirected to print output, offscreen buffers etc.
	 *  @param graphics New graphics context in which the modular class is embedded.
	 */
	public void setGraphics(PGraphics graphics)
	{
		this.graphics = graphics;
	}
	
	/** Sets the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @param order Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public void setOrder(int order)
	{
		this.order = order;
	}
	
	/** Reports the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @return Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public int getOrder()
	{
		return order;
	}
	
	/** Would provide some text output that can be written to the given file but in this case does nothing.
	 *  @param fileName Name of output file (ignored).
	 *  @return True if output is written successfully, but will always be false while this method does not write any output.
	 */
	public boolean writeOutput(String fileName)
	{
		// Do nothing.
		return false;
	}

	/** Draws the user interface for the variogram calculator.
	 */
	public void draw(float xOrigin, float yOrigin, float width, float height)
	{		
		if (builders.size() > 0)
		{
			// Display holding screen while inclination charts are being calculated.
			waiter.draw(xOrigin, yOrigin, width, height);
			return;
		}
		if (waiter != null)
		{
			waiter.stop();
			waiter = null;
		}
		
		// Check to see that we have at least one DEM to display		
		if (inclinationImgs.size() == 0)
		{
			graphics.fill(graphics.color(80));
			graphics.textFont(DisplayUtils.mediumFont);
			graphics.textAlign(PConstants.CENTER, PConstants.CENTER);
			graphics.text("No DEMs available for inclination chart display",xOrigin+width/2,yOrigin+height/2);
			return;
		}

		Rectangle2D.Float[] rects = DisplayUtils.getBoundaries(xOrigin, yOrigin, width, height, inclinationImgs.size(),LayoutType.ROW);
		graphics.imageMode(PConstants.CENTER);
		float weightScale = width/600;
		
		for (int i=0; i<inclinationImgs.size(); i++)
		{
			PImage img = inclinationImgs.get(new Integer(i));
			if (img == null)
			{
				continue;
			}
			
			VectorMap contourMap = contourMaps.get(new Integer(i));
			if (inclinationImgs.size() == 1)
			{
				graphics.fill(0);
			}
			else
			{
				graphics.fill(DisplayUtils.getCategoryPalette().findColour(i));
			}
			graphics.textFont(DisplayUtils.mediumFont);
			float textHeight = graphics.textAscent()+graphics.textDescent();
			float scaling = Math.min((rects[i].width-10)/img.width,(rects[i].height-10-textHeight)/img.height);
			float imageWidth  = img.width*scaling;
			float imageHeight = img.height*scaling;
			float interval = img.width*scaling/6f;
			
			float cx = rects[i].x + rects[i].width/2;
			float cy = rects[i].y + rects[i].height/2-textHeight/2;
			
			// Inclination chart raster.
			graphics.image(img,cx,cy,imageWidth,imageHeight);
			
			// Reduce impact of image. Cannot use tint as this is incompatible with PDF output.
			graphics.fill(255,90);
			graphics.rectMode(PConstants.CENTER);
			graphics.noStroke();
			graphics.rect(cx,cy,imageWidth,imageHeight);
			
			// Lag scale.
			graphics.noFill();
			graphics.stroke(0,0,100,40);
			graphics.strokeWeight(weightScale);
			graphics.line(rects[i].x+(rects[i].width-imageWidth)/2,cy,rects[i].x+(rects[i].width-imageWidth)/2+imageWidth,cy);
			graphics.line(cx, rects[i].y+(rects[i].height-textHeight-imageHeight)/2,cx, rects[i].y+(rects[i].height-textHeight-imageHeight)/2+imageHeight);
						
			for (float r=interval; r<imageWidth+interval/2; r+=interval)
			{
				graphics.ellipse(cx,cy,r,r);
			}
			
			// Inclination chart contours.
			DisplayUtils.drawContours(contourMap, cx,cy,imageWidth,imageHeight,weightScale,graphics);
			
			
			// Show title of raster for which inclination chart is displayed, scaled if necessary to fit within display bounds.
			graphics.textSize(12);
			graphics.fill(DisplayUtils.getCategoryPalette().findColour(colourOffset+i));
			String title = "2D Inclination Chart: "+titles.get(new Integer(i));
			float scaleFactor = Math.min(1,(rects[i].width-textHeight)/graphics.textWidth(title));
			
			graphics.pushMatrix();
			graphics.translate(rects[i].x+rects[i].width/2f,rects[i].y + (rects[i].height+img.height*scaling+textHeight)/2);
			graphics.scale(scaleFactor,scaleFactor);
						
			graphics.textAlign(PConstants.CENTER,PConstants.BOTTOM);
			graphics.text(title,0,0);
			graphics.popMatrix();
		}
	}
	
	/** Adds the given DEM to those that will be displayed as inclination charts.
	 *  @param dem DEM to add.
	 */
	public void addDEM(RasterMap dem) 
	{
		Integer index = new Integer(nextIndex);
		nextIndex++;			// Increment index of DEM so that order of calls to addDEM determines order, not thread completion speed.
		
		if (dem == null)
		{
			inclinationImgs.put(index,null);
			titles.put(index,"");
		}
		else
		{
			maxLagCells = (int)(150f/dem.getXRes());
			InclinationBuilder vBuilder = new InclinationBuilder(dem, maxLagCells,index);
			vBuilder.start();
			titles.put(index,dem.getHeader().getTitle());
		}
	}
	
	/** Sets the colour palette start colour for text display.
	 *  @param offset Position in default colour palette in which the first variogram text is displayed.
	 */
	public void setColourOffset(int offset)
	{
		this.colourOffset = offset;
	}
	
	// ------------------------ Mouse and keyboard handling ------------------------
	
	/** Would respond to mouse press events but does nothing in this case.
	 */
	public void mousePressed()
	{
		// No mouse events handled.
	}

	/** Would respond to mouse release events but does nothing in this case.
	 */
	public void mouseReleased()
	{
		// No mouse events handled.
	}

	/** Would respond to mouse drag events but does nothing in this case.
	 */
	public void mouseDragged()
	{
		// No mouse events handled.
	}
	
	/** Allows this module to be compared with another to determine sort order.
	 *  @param otherModule Other module to compare with this one.
	 */
	public int compareTo(Modular otherModule)
	{
		int comparison = order-otherModule.getOrder();
		if (comparison == 0)
		{
			return hashCode()-otherModule.hashCode();
		}
		return comparison;
	}

	// ------------------------------ Private methods ------------------------------

	/** Creates a contour vector map from the given LandSerf raster. 
	 *  @param raster Raster used to create contours.
	 *  @return Vector contours.
	 */
	private VectorMap createContourMap(RasterMap raster)
	{
		// Generate contour vectors
		GISFrame gisFrame = new SimpleGISFrame();

		int origWindowSize = raster.getWSize();
		raster.setWSize(15);		
		gisFrame.addRaster(raster, GISFrame.PRIMARY);

		// Smooth raster.
		SurfParam surfParam = new SurfParam(gisFrame, RasterMap.ELEV);
		surfParam.findParam();
		gisFrame.setRaster1(surfParam.getSurfParam());
		raster.setWSize(origWindowSize);

		float extreme = Math.max(Math.abs(gisFrame.getRaster1().getMinAttribute()), Math.abs(gisFrame.getRaster1().getMaxAttribute()));
		ContourThread contourThread = new ContourThread(gisFrame,-extreme,extreme/20,2);
		
		//float minVal = gisFrame.getRaster1().getMinAttribute();
		//float range = gisFrame.getRaster1().getMaxAttribute()-minVal;
		//ContourThread contourThread = new ContourThread(gisFrame,minVal,range/20f,2);
		//ContourThread contourThread = new ContourThread(gisFrame,0,0.5f,2);
		contourThread.start();
		try
		{
			contourThread.join();    // Join thread (i.e. wait until it is complete).
		}
		catch (InterruptedException e)
		{
			System.err.println("Error: Contouring thread interrupted.");
		}
		return gisFrame.getVectorMap1();
	}

	
	// ------------------------------ Nested classes -------------------------------
	
	/** Class to build an inclination chart from a random selection of cell pairs from a raster.
	 *  Since this can be computationally expensive and time consuming, the calculation is spawned
	 *  as a threaded process.
	 */
	private class InclinationBuilder extends Thread
	{
		private RasterMap raster;			// Raster from which to build inclination chart.
		private int maxLag;
		private Integer index;
				
		/** Initialise the class with the given raster.
		 *  @param raster Raster DEM from which inclination chart is to be calculated. 
		 *  @param maxLag The maximum lag of the inclination chart in grid cells.
		 */
		public InclinationBuilder(RasterMap raster, int maxLag, Integer index)
		{
			this.raster = raster;
			this.maxLag = maxLag;
			this.index = index;
		}
		
		/** Builds the inclination chart from a random selection of cell pairs from a raster.
		 */
		@SuppressWarnings("synthetic-access")
		public void run()
		{
			builders.add(this);
			if (waiter == null)
			{
				waiter = new Waiter(parent);
				waiter.setMessage("Calculating inclination chart");
			}
			
			int inc = 5;						// Process every nth row/column.
			int numSamples = 10000;

			RasterMap inclinationRast = new RasterMap(2*maxLag/inc,2*maxLag/inc);

			for (int yLag=0; yLag <=maxLag; yLag+=inc)
			{
				int startRow = Math.max(0,-yLag);
				int endRow   = raster.getNumRows()-1-Math.max(0,yLag);
				
				waiter.setMessage("Calculating inclination\n"+(100*yLag/maxLag)+"% complete.");
				
				for (int xLag=-maxLag; xLag <=maxLag; xLag+=inc)	
				{	
					InclinationFinder inclination = new InclinationFinder();

					int startCol = Math.max(0,-xLag);
					int endCol   = raster.getNumCols()-1-Math.max(0,xLag);
					double lag = Math.sqrt(xLag*xLag + yLag*yLag);

					// Choose a set of random samples at each lag and direction.
					for (int sample=0; sample<numSamples; sample++)
					{
						int row = (int)parent.random(startRow,endRow);
						int col = (int)parent.random(startCol,endCol);

						float z1 = raster.getAttribute(row, col);
						float z2 = raster.getAttribute(row+yLag,col+xLag);

						if ((z1 != SpatialModel.NO_VALUE) && (z2 != SpatialModel.NO_VALUE))
						{
							inclination.add(z1, z2, lag);
						}
					}

					// Add inclination index to the raster cell corresponding to this particular lag and direction.
					inclinationRast.setAttribute((maxLag+yLag)/inc, (maxLag+xLag)/inc, inclination.getInclinationIndex());
					inclinationRast.setAttribute((maxLag-yLag)/inc, (maxLag-xLag)/inc, inclination.getInclinationIndex());
				}	
			}

			//  Add metadata and save raster
			inclinationRast.getHeader().setTitle("2D Variogram");
			inclinationRast.getHeader().setNotes("2D semivariogram of "+raster.getHeader().getTitle());
			setNormalColourTable(inclinationRast);
			inclinationRast.setXRes(raster.getXRes()*inc);
			inclinationRast.setYRes(raster.getYRes()*inc);
			inclinationRast.setBounds(new Footprint(-maxLag*raster.getXRes(),-maxLag*raster.getYRes(),
					                                inclinationRast.getNumCols()*inclinationRast.getXRes(),
					                                inclinationRast.getNumRows()*inclinationRast.getYRes()));
			inclinationImgs.put(index,DisplayUtils.createImage(parent,inclinationRast, RasterMap.ELEV));
			contourMaps.put(index,createContourMap(inclinationRast));
//LandSerfIO.write(inclinationRast, "incRaster.srf");			
			raster = null;			// Permit garbage collection of the object variable once thread is complete.
			builders.remove(this);
			parent.loop();			// Refresh screen once calculation is complete.
		}
	}
	
	/** Creates an red-orange colour scheme based on a normal distribution around inclination values.
	 *  @param raster Raster to use the new colour table.
	 */
	private void setNormalColourTable(RasterMap raster)
	{		
		// One pass calculation of mean and standard deviation from Wikipedia
		// http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
		double mean = 0;
		double stdev = 0;
		int numSamples = 0;
		double M2 = 0;
		double delta,delta_n,term1;

		for (int row=0; row<raster.getNumRows();row++)
		{
			for (int col=0; col<raster.getNumCols();col++)
			{
				float z = raster.getAttribute(row,col);

				if (z != SpatialModel.NO_VALUE)
				{	
					numSamples++;
					delta = z-mean;
					delta_n = delta / numSamples;
					term1 = delta*delta_n*(numSamples-1);
					mean =  mean + delta_n;
					M2 = M2 + term1;
				}
			}
		}
		
		stdev = Math.sqrt(M2/numSamples);
		
		ColourTable cTable = new ColourTable();
		cTable.addContinuousColourRule((float)(mean+4*stdev), 127,0,0);
		cTable.addContinuousColourRule((float)(mean+3*stdev), 179,0,0);
		cTable.addContinuousColourRule((float)(mean+2*stdev), 215,48,31);
		cTable.addContinuousColourRule((float)(mean+stdev), 239,101,72);
		cTable.addContinuousColourRule((float)(mean-stdev), 252,141,89);
		cTable.addContinuousColourRule((float)(mean-2*stdev), 253,187,132);
		cTable.addContinuousColourRule((float)(mean-3*stdev), 253,212,158);
		cTable.addContinuousColourRule((float)(mean-4*stdev), 254,232,200);
		cTable.addContinuousColourRule((float)(mean-5*stdev), 255,247,236);
		
		raster.setColourTable(cTable);
	}
}